package userinterface;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.SocketException;
import java.net.UnknownHostException;

import exceptions.ArgumentException;

import protocol.channels.control.ReceiverControlChannel;
import protocol.channels.control.SenderControlChannel;

/**
 * The application user interface
 */
public class UserInterface {
	
	/**
	 * Used to read user input
	 */
	private BufferedReader reader;
	/**
	 * The local control channel
	 */
	private ReceiverControlChannel receiver;
	/**
	 * The remove control channel
	 */
	private SenderControlChannel sender;
	
	private BufferedReader getReader() {
		return this.reader;
	}
	
	private void setReader(BufferedReader reader) {
		this.reader = reader;
	}
	
	private ReceiverControlChannel getReceiver() {
		return this.receiver;
	}

	private void setReceiver(ReceiverControlChannel receiver) {
		this.receiver = receiver;
	}

	private SenderControlChannel getSender() {
		return this.sender;
	}

	private void setSender(SenderControlChannel sender) {
		this.sender = sender;
	}

	/**
	 * Main program
	 * @param args Command line arguments, not used
	 */
	public static void main(String[] args) {
		UserInterface ui = new UserInterface();
		/**
		 * Open the local control channel
		 */
		ui.openLocalControlChannel();
		/**
		 * Connect to the remote control channel
		 */
		ui.connectToRemoteControlChannel();
		System.out.println("Initializing the connection (3-way handshake), please wait.. ");
		new Thread(ui.getReceiver()).start();
		new Thread(ui.getSender()).start();
		/**
		 * Wait until the 3-way handshake is completed
		 */
		while (!ui.getSender().getThreeWayHandshakeCompleted()) {
			Thread.yield();
		}
		System.out.println("Connection initialized!");
		ui.waitForCommand();
	}

	/**
	 * 
	 */
	private void waitForCommand() {
		this.showCommands();
		while (true) {
			try {
				String command = this.ask(">");
				if (command == null) continue;
				if (command.equalsIgnoreCase("exit")) {
					System.out.println("Program closing..");
					System.exit(0);
				} else if (command.equalsIgnoreCase("receive")) {
					this.requestFile();
				} else if (command.equalsIgnoreCase("send")) {
					this.sendFile();
				} else {
					System.out.println("ERROR: invalid command!");
					this.showCommands();
				}
			} catch (IOException e) {
				System.err.println(String.format("ERROR: %s", e.toString()));
				System.err.println("Program closing..");
				System.exit(0);
			}
			Thread.yield();
		}
	}
	
	/**
	 * Shows a list of supported commands
	 */
	private void showCommands() {
		System.out.println("Commands:");
		System.out.println("Type 'send' to send a file");
		System.out.println("Type 'receive' to get a file");
		System.out.println("Type 'exit' to exit the program");
	}
	
	/**
	 * Opens the local control channel.
	 */
	private void openLocalControlChannel() {
		this.setReceiver(new ReceiverControlChannel());
		try {
			/**
			 * Ask the used host name and port
			 */
			String host = this.ask("Local control channel host >");
			int port = this.askPort("Local control channel port >");
			/**
			 * Attempt to open the control channel
			 */
			System.out.println("Opening local control channel..");
			this.getReceiver().listen(host, port);
			System.out.println("Local control channel opened!");
		} catch (SocketException e) {
			System.out.println("ERROR: The given port is already in use.");
			this.openLocalControlChannel();
		} catch (UnknownHostException e) {
			System.out.println("ERROR: Invalid local host name.");
			this.openLocalControlChannel();
		} catch (IOException e) {
			System.out.println(String.format("ERROR: %s", e.toString()));
			System.out.println("Program closing..");
			System.exit(0);
		}
	}
	
	/**
	 * Used to send a new file request
	 */
	private void requestFile() {
		try {
			String filename = this.askFilename("Request file >");
			this.getSender().requestFile(filename);
			System.out.println(String.format("Requesting file %s ..", filename));
		} catch (IOException e) {
			System.out.println(String.format("Error: %s", e.toString()));
			System.out.println("Program closing..");
			System.exit(0);
		} catch (ArgumentException e) {
			System.out.println(String.format("ERROR: %s", e.toString()));
			this.requestFile();
		}
	}
	
	/**
	 * Used to send a file
	 */
	private void sendFile() {
		try {
			String filename = this.askFilename("The file to be sent >");
			this.getSender().sendFile(filename);
			System.out.println(String.format("Sending file %s ..", filename));
		} catch (ArgumentException e) {
			System.out.println(String.format("ERROR: %s", e.toString()));
			this.sendFile();
		} catch (IOException e) {
			System.out.println(String.format("Error: %s", e.toString()));
			System.out.println("Program closing..");
			System.exit(0);
		}
	}
	
	/**
	 * Opens the remote control channel
	 */
	private void connectToRemoteControlChannel() {
		this.setSender(new SenderControlChannel());
		try {
			/**
			 * Ask the host name and port
			 */
			String host = this.ask("Remote control channel host >");
			int port = this.askPort("Remote control channel port >");
			/**
			 * Attempt to connect to the given host in the given port
			 */
			System.out.println("Opening remote control channel..");
			this.getSender().connect(host, port);
			System.out.println("Remote control channel opened!");
		} catch (UnknownHostException e) {
			System.out.println("ERROR: Unknown remote host name!");
			this.connectToRemoteControlChannel();			
		} catch (IOException e) {
			System.out.println(String.format("Error: %s", e.toString()));
			System.out.println("Program closing..");
			System.exit(0);
		}
	}
	
	/**
	 * Asks the user a question and returns the answer (one line of text)
	 * @param question The question shown for the user
	 * @return The given answer (one line of text, \r\n\ is not included)
	 * @throws IOException if any I/O errors occur
	 */
	private String ask(String question) throws IOException {
		if (this.getReader() == null) {
			this.setReader(new BufferedReader(new InputStreamReader(System.in)));
		}
		System.out.print(question);
		return this.getReader().readLine();
	}

	/**
	 * Asks the user a question and returns the answer (an integer).
	 * If the user does not input an integer, the question is repeated.
	 * @param question The question shown for the user
	 * @return The given answer (an integer)
	 * @throws IOException If any I/O errors occur
	 */
	private int askInt(String question) throws IOException {
		if (this.getReader() == null) {
			this.setReader(new BufferedReader(new InputStreamReader(System.in)));
		}
		try {
			return Integer.parseInt(this.ask(question));
		} catch (NumberFormatException e) {
			System.out.println("Please input an integer");
			return this.askInt(question);
		}
	}
	
	/**
	 * Asks the user to give a valid port number (a number between 0-65535)
	 * @param The shown question for the user
	 * @return The given port number
	 * @throws IOException if any I/O errors occur
	 */
	private int askPort(String question) throws IOException {
		int port = this.askInt(question);
		if (port >= 0 && port <= 65535) {
			return port;
		} else {
			System.out.println("Invalid port number. Please input an integer between 0..65535");
			return this.askPort(question);
		}
	}
	
	/**
	 * Asks the user to give a valid filename (String of the length [1, n])
	 * @param question The shown question for the user
	 * @return The given filename
	 * @throws IOException If any I/O errors occur
	 */
	private String askFilename(String question) throws IOException {
		String filename = this.ask(question);
		if (filename.length() == 0) {
			System.out.println("Invalid filename. Please input at least one character.");
			return this.askFilename(question);
		} else {
			return filename;
		}
	}
	
}